/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to you under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.calcite.sql;

import com.google.common.base.Predicates;
import com.google.common.base.Utf8;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterators;
import com.google.common.collect.Lists;

import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.charset.UnsupportedCharsetException;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.function.Predicate;
import java.util.stream.Collectors;

import org.apache.calcite.avatica.util.ByteString;
import org.apache.calcite.linq4j.Ord;
import org.apache.calcite.linq4j.function.Functions;
import org.apache.calcite.rel.RelNode;
import org.apache.calcite.rel.hint.HintStrategyTable;
import org.apache.calcite.rel.hint.Hintable;
import org.apache.calcite.rel.hint.RelHint;
import org.apache.calcite.rel.type.RelDataType;
import org.apache.calcite.rel.type.RelDataTypeFactory;
import org.apache.calcite.rel.type.RelDataTypePrecedenceList;
import org.apache.calcite.runtime.CalciteContextException;
import org.apache.calcite.runtime.CalciteException;
import org.apache.calcite.runtime.Resources;
import org.apache.calcite.sql.SqlWriter.FrameTypeEnum;
import org.apache.calcite.sql.fun.SqlInOperator;
import org.apache.calcite.sql.fun.SqlStdOperatorTable;
import org.apache.calcite.sql.parser.SqlParserPos;
import org.apache.calcite.sql.type.SqlOperandMetadata;
import org.apache.calcite.sql.type.SqlOperandTypeChecker;
import org.apache.calcite.sql.type.SqlTypeName;
import org.apache.calcite.sql.type.SqlTypeUtil;
import org.apache.calcite.sql.util.SqlBasicVisitor;
import org.apache.calcite.sql.util.SqlVisitor;
import org.apache.calcite.sql.validate.SqlNameMatcher;
import org.apache.calcite.sql.validate.SqlValidatorUtil;
import org.apache.calcite.util.BarfingInvocationHandler;
import org.apache.calcite.util.ConversionUtil;
import org.apache.calcite.util.Litmus;
import org.apache.calcite.util.NlsString;
import org.apache.calcite.util.Pair;
import org.apache.calcite.util.Static;
import org.apache.calcite.util.Util;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.checkerframework.checker.nullness.qual.PolyNull;
import org.slf4j.Logger;

/**
 * replace org.apache.calcite.sql.SqlUtil for the TABLE("READ_CSV") -> READ_CSV
 */
public abstract class SqlUtil {
    public static final String GENERATED_EXPR_ALIAS_PREFIX = "EXPR$";

    public SqlUtil() {
    }

    public static SqlNode andExpressions(@Nullable SqlNode node1, SqlNode node2) {
        if (node1 == null) {
            return node2;
        } else {
            ArrayList<SqlNode> list = new ArrayList();
            if (node1.getKind() == SqlKind.AND) {
                list.addAll(((SqlCall) node1).getOperandList());
            } else {
                list.add(node1);
            }

            if (node2.getKind() == SqlKind.AND) {
                list.addAll(((SqlCall) node2).getOperandList());
            } else {
                list.add(node2);
            }

            return SqlStdOperatorTable.AND.createCall(SqlParserPos.ZERO, list);
        }
    }

    static ArrayList<SqlNode> flatten(SqlNode node) {
        ArrayList<SqlNode> list = new ArrayList();
        flatten(node, list);
        return list;
    }

    public static SqlNode getFromNode(SqlSelect query, int ordinal) {
        SqlNode from = query.getFrom();
        if (from == null) {
            throw new AssertionError("from must not be null for " + query);
        } else {
            ArrayList<SqlNode> list = flatten(from);
            return (SqlNode) list.get(ordinal);
        }
    }

    private static void flatten(SqlNode node, ArrayList<SqlNode> list) {
        switch (node.getKind()) {
            case JOIN:
                SqlJoin join = (SqlJoin) node;
                flatten(join.getLeft(), list);
                flatten(join.getRight(), list);
                return;
            case AS:
                SqlCall call = (SqlCall) node;
                flatten(call.operand(0), list);
                return;
            default:
                list.add(node);
        }
    }

    public static SqlNodeList toNodeList(SqlNode[] operands) {
        SqlNodeList ret = new SqlNodeList(SqlParserPos.ZERO);
        Collections.addAll(ret, operands);
        return ret;
    }

    public static int indexOfDeep(List<? extends SqlNode> list, SqlNode e, Litmus litmus) {
        for (int i = 0; i < list.size(); ++i) {
            if (e.equalsDeep((SqlNode) list.get(i), litmus)) {
                return i;
            }
        }

        return -1;
    }

    public static boolean isNullLiteral(@Nullable SqlNode node, boolean allowCast) {
        if (node instanceof SqlLiteral) {
            SqlLiteral literal = (SqlLiteral) node;
            if (literal.getTypeName() == SqlTypeName.NULL) {
                assert null == literal.getValue();

                return true;
            } else {
                return false;
            }
        } else if (allowCast && node != null && node.getKind() == SqlKind.CAST) {
            SqlCall call = (SqlCall) node;
            return isNullLiteral(call.operand(0), false);
        } else {
            return false;
        }
    }

    public static boolean isNull(SqlNode node) {
        return isNullLiteral(node, false) || node.getKind() == SqlKind.CAST && isNull(((SqlCall) node).operand(0));
    }

    public static boolean isLiteral(SqlNode node, boolean allowCast) {
        Objects.requireNonNull(node, "node");
        if (node instanceof SqlLiteral) {
            return true;
        } else if (!allowCast) {
            return false;
        } else {
            switch (node.getKind()) {
                case CAST:
                    return isLiteral(((SqlCall) node).operand(0), true);
                case MAP_VALUE_CONSTRUCTOR:
                case ARRAY_VALUE_CONSTRUCTOR:
                    return ((SqlCall) node).getOperandList().stream().allMatch((o) -> isLiteral(o, true));
                case DEFAULT:
                    return true;
                default:
                    return false;
            }
        }
    }

    public static boolean isLiteral(SqlNode node) {
        return isLiteral(node, false);
    }

    public static boolean isLiteralChain(SqlNode node) {
        Objects.requireNonNull(node, "node");
        if (node instanceof SqlCall) {
            SqlCall call = (SqlCall) node;
            return call.getKind() == SqlKind.LITERAL_CHAIN;
        } else {
            return false;
        }
    }

    /**
     * @deprecated
     */
    @Deprecated
    public static void unparseFunctionSyntax(SqlOperator operator, SqlWriter writer, SqlCall call) {
        unparseFunctionSyntax(operator, writer, call, false);
    }

    public static void unparseFunctionSyntax(SqlOperator operator, SqlWriter writer, SqlCall call, boolean ordered) {
        if (operator instanceof SqlFunction) {
            SqlFunction function = (SqlFunction) operator;
            if (function.getFunctionType().isSpecific()) {
                writer.keyword("SPECIFIC");
            }

            SqlIdentifier id = function.getSqlIdentifier();
            if (id == null) {
                writer.keyword(operator.getName());
            } else {
                unparseSqlIdentifierSyntax(writer, id, true);
            }
        } else {
            // remove TABLE part
            if (!"table".equalsIgnoreCase(operator.getName())) {
                writer.print(operator.getName());
            }
        }

        if (call.operandCount() == 0) {
            switch (call.getOperator().getSyntax()) {
                case FUNCTION_ID:
                    return;
                case FUNCTION_STAR:
                case FUNCTION:
                case ORDERED_FUNCTION:
            }
        }
        SqlWriter.Frame frame = null;
        if (!"table".equalsIgnoreCase(operator.getName())) {
            frame = writer.startList(FrameTypeEnum.FUN_CALL, "(", ")");
        }
        SqlLiteral quantifier = call.getFunctionQuantifier();
        if (quantifier != null) {
            quantifier.unparse(writer, 0, 0);
        }

        if (call.operandCount() == 0) {
            switch (call.getOperator().getSyntax()) {
                case FUNCTION_STAR:
                    writer.sep("*");
            }
        }

        for (SqlNode operand : call.getOperandList()) {
            if (ordered && operand instanceof SqlNodeList) {
                writer.sep("ORDER BY");
            } else {
                if (ordered && operand.getKind() == SqlKind.SEPARATOR) {
                    writer.sep("SEPARATOR");
                    ((SqlCall) operand).operand(0).unparse(writer, 0, 0);
                    continue;
                }

                writer.sep(",");
            }

            operand.unparse(writer, 0, 0);
        }

        if (Objects.nonNull(frame)) {
            writer.endList(frame);
        }
    }

    public static void unparseSqlIdentifierSyntax(SqlWriter writer, SqlIdentifier identifier, boolean asFunctionID) {
        boolean isUnquotedSimple = identifier.isSimple() && !identifier.getParserPosition().isQuoted();
        SqlOperator operator = isUnquotedSimple ? SqlValidatorUtil.lookupSqlFunctionByID(SqlStdOperatorTable.instance(), identifier, (SqlFunctionCategory) null) : null;
        boolean unparsedAsFunc = false;
        SqlWriter.Frame frame = writer.startList(FrameTypeEnum.IDENTIFIER);
        if (isUnquotedSimple && operator != null && (asFunctionID || operator.getSyntax() == SqlSyntax.FUNCTION_ID)) {
            writer.keyword(identifier.getSimple());
            unparsedAsFunc = true;
        }

        if (!unparsedAsFunc) {
            for (int i = 0; i < identifier.names.size(); ++i) {
                writer.sep(".");
                String name = (String) identifier.names.get(i);
                SqlParserPos pos = identifier.getComponentParserPosition(i);
                if (name.isEmpty()) {
                    writer.print("*");
                    writer.setNeedWhitespace(true);
                } else {
                    writer.identifier(name, pos.isQuoted());
                }
            }
        }

        if (null != identifier.getCollation()) {
            identifier.getCollation().unparse(writer);
        }

        writer.endList(frame);
    }

    public static void unparseBinarySyntax(SqlOperator operator, SqlCall call, SqlWriter writer, int leftPrec, int rightPrec) {
        assert call.operandCount() == 2;

        SqlWriter.Frame frame = writer.startList(operator instanceof SqlSetOperator ? FrameTypeEnum.SETOP : FrameTypeEnum.SIMPLE);
        call.operand(0).unparse(writer, leftPrec, operator.getLeftPrec());
        boolean needsSpace = operator.needsSpace();
        writer.setNeedWhitespace(needsSpace);
        writer.sep(operator.getName());
        writer.setNeedWhitespace(needsSpace);
        call.operand(1).unparse(writer, operator.getRightPrec(), rightPrec);
        writer.endList(frame);
    }

    public static SqlLiteral concatenateLiterals(List<SqlLiteral> lits) {
        return (SqlLiteral) (lits.size() == 1 ? (SqlLiteral) lits.get(0) : ((SqlAbstractStringLiteral) lits.get(0)).concat1(lits));
    }

    public static @Nullable SqlOperator lookupRoutine(SqlOperatorTable opTab, RelDataTypeFactory typeFactory, SqlIdentifier funcName, List<RelDataType> argTypes, @Nullable List<String> argNames, @Nullable SqlFunctionCategory category, SqlSyntax syntax, SqlKind sqlKind, SqlNameMatcher nameMatcher, boolean coerce) {
        Iterator<SqlOperator> list = lookupSubjectRoutines(opTab, typeFactory, funcName, argTypes, argNames, syntax, sqlKind, category, nameMatcher, coerce);
        return list.hasNext() ? (SqlOperator) list.next() : null;
    }

    private static Iterator<SqlOperator> filterOperatorRoutinesByKind(Iterator<SqlOperator> routines, SqlKind sqlKind) {
        return Iterators.filter(routines, (operator) -> ((SqlOperator) Objects.requireNonNull(operator, "operator")).getKind().getFunctionKind() == sqlKind);
    }

    public static Iterator<SqlOperator> lookupSubjectRoutines(SqlOperatorTable opTab, RelDataTypeFactory typeFactory, SqlIdentifier funcName, List<RelDataType> argTypes, @Nullable List<String> argNames, SqlSyntax sqlSyntax, SqlKind sqlKind, @Nullable SqlFunctionCategory category, SqlNameMatcher nameMatcher, boolean coerce) {
        Iterator<SqlOperator> routines = lookupSubjectRoutinesByName(opTab, funcName, sqlSyntax, category, nameMatcher);
        routines = filterRoutinesByParameterCount(routines, argTypes);
        if (category == SqlFunctionCategory.USER_DEFINED_PROCEDURE) {
            return routines;
        } else {
            routines = filterRoutinesByParameterTypeAndName(typeFactory, sqlSyntax, routines, argTypes, argNames, coerce);
            List<SqlOperator> list = Lists.newArrayList(routines);
            routines = list.iterator();
            if (list.size() >= 2 && !coerce) {
                routines = filterRoutinesByTypePrecedence(sqlSyntax, typeFactory, routines, argTypes, argNames);
                return filterOperatorRoutinesByKind(routines, sqlKind);
            } else {
                return routines;
            }
        }
    }

    public static boolean matchRoutinesByParameterCount(SqlOperatorTable opTab, SqlIdentifier funcName, List<RelDataType> argTypes, SqlFunctionCategory category, SqlNameMatcher nameMatcher) {
        Iterator<SqlOperator> routines = lookupSubjectRoutinesByName(opTab, funcName, SqlSyntax.FUNCTION, category, nameMatcher);
        routines = filterRoutinesByParameterCount(routines, argTypes);
        return routines.hasNext();
    }

    private static Iterator<SqlOperator> lookupSubjectRoutinesByName(SqlOperatorTable opTab, SqlIdentifier funcName, SqlSyntax syntax, @Nullable SqlFunctionCategory category, SqlNameMatcher nameMatcher) {
        List<SqlOperator> sqlOperators = new ArrayList();
        opTab.lookupOperatorOverloads(funcName, category, syntax, sqlOperators, nameMatcher);
        switch (syntax) {
            case FUNCTION:
                return Iterators.filter(sqlOperators.iterator(), Predicates.instanceOf(SqlFunction.class));
            default:
                return Iterators.filter(sqlOperators.iterator(), (operator) -> ((SqlOperator) Objects.requireNonNull(operator, "operator")).getSyntax() == syntax);
        }
    }

    private static Iterator<SqlOperator> filterRoutinesByParameterCount(Iterator<SqlOperator> routines, List<RelDataType> argTypes) {
        return Iterators.filter(routines, (operator) -> ((SqlOperator) Objects.requireNonNull(operator, "operator")).getOperandCountRange().isValidCount(argTypes.size()));
    }

    private static Iterator<SqlOperator> filterRoutinesByParameterTypeAndName(RelDataTypeFactory typeFactory, SqlSyntax syntax, Iterator<SqlOperator> routines, List<RelDataType> argTypes, @Nullable List<String> argNames, boolean coerce) {
        return (Iterator<SqlOperator>) (syntax != SqlSyntax.FUNCTION ? routines : Iterators.filter(Iterators.filter(routines, SqlFunction.class), (function) -> {
            SqlOperandTypeChecker operandTypeChecker = ((SqlFunction) Objects.requireNonNull(function, "function")).getOperandTypeChecker();
            if (operandTypeChecker != null && operandTypeChecker.isFixedParameters()) {
                SqlOperandMetadata operandMetadata = (SqlOperandMetadata) operandTypeChecker;
                List<RelDataType> paramTypes = operandMetadata.paramTypes(typeFactory);
                List<RelDataType> permutedArgTypes;
                if (argNames != null) {
                    List<String> paramNames = operandMetadata.paramNames();
                    permutedArgTypes = permuteArgTypes(paramNames, argNames, argTypes);
                    if (permutedArgTypes == null) {
                        return false;
                    }
                } else {
                    permutedArgTypes = Lists.newArrayList(argTypes);

                    while (permutedArgTypes.size() < argTypes.size()) {
                        paramTypes.add(null);
                    }
                }

                for (Pair<RelDataType, RelDataType> p : Pair.zip(paramTypes, permutedArgTypes)) {
                    RelDataType argType = (RelDataType) p.right;
                    RelDataType paramType = (RelDataType) p.left;
                    if (argType != null && paramType != null && !SqlTypeUtil.canCastFrom(paramType, argType, coerce)) {
                        return false;
                    }
                }

                return true;
            } else {
                return true;
            }
        }));
    }

    private static List<RelDataType> permuteArgTypes(List<String> paramNames, List<String> argNames, List<RelDataType> argTypes) {
        Map<Integer, Integer> map = new HashMap();

        for (Ord<String> argName : Ord.zip(argNames)) {
            int i = paramNames.indexOf(argName.e);
            if (i < 0) {
                return null;
            }

            map.put(i, argName.i);
        }

        return Functions.generate(paramNames.size(), (index) -> {
            Integer argIndex = (Integer) map.get(index);
            return argIndex != null ? (RelDataType) argTypes.get(argIndex) : null;
        });
    }

    private static Iterator<SqlOperator> filterRoutinesByTypePrecedence(SqlSyntax sqlSyntax, RelDataTypeFactory typeFactory, Iterator<SqlOperator> routines, List<RelDataType> argTypes, @Nullable List<String> argNames) {
        if (sqlSyntax != SqlSyntax.FUNCTION) {
            return routines;
        } else {
            List<SqlFunction> sqlFunctions = Lists.newArrayList(Iterators.filter(routines, SqlFunction.class));

            for (Ord<RelDataType> argType : Ord.zip(argTypes)) {
                RelDataTypePrecedenceList precList = ((RelDataType) argType.e).getPrecedenceList();
                RelDataType bestMatch = bestMatch(typeFactory, sqlFunctions, argType.i, argNames, precList);
                if (bestMatch != null) {
                    sqlFunctions = (List) sqlFunctions.stream().filter((function) -> {
                        SqlOperandTypeChecker operandTypeChecker = function.getOperandTypeChecker();
                        if (operandTypeChecker != null && operandTypeChecker.isFixedParameters()) {
                            SqlOperandMetadata operandMetadata = (SqlOperandMetadata) operandTypeChecker;
                            List<String> paramNames = operandMetadata.paramNames();
                            List<RelDataType> paramTypes = operandMetadata.paramTypes(typeFactory);
                            int index = argNames != null ? paramNames.indexOf(argNames.get(argType.i)) : argType.i;
                            RelDataType paramType = (RelDataType) paramTypes.get(index);
                            return precList.compareTypePrecedence(paramType, bestMatch) >= 0;
                        } else {
                            return false;
                        }
                    }).collect(Collectors.toList());
                }
            }

            return (Iterator) sqlFunctions.iterator();
        }
    }

    private static @Nullable RelDataType bestMatch(RelDataTypeFactory typeFactory, List<SqlFunction> sqlFunctions, int i, @Nullable List<String> argNames, RelDataTypePrecedenceList precList) {
        RelDataType bestMatch = null;

        for (SqlFunction function : sqlFunctions) {
            SqlOperandTypeChecker operandTypeChecker = function.getOperandTypeChecker();
            if (operandTypeChecker != null && operandTypeChecker.isFixedParameters()) {
                SqlOperandMetadata operandMetadata = (SqlOperandMetadata) operandTypeChecker;
                List<RelDataType> paramTypes = operandMetadata.paramTypes(typeFactory);
                List<String> paramNames = operandMetadata.paramNames();
                RelDataType paramType = argNames != null ? (RelDataType) paramTypes.get(paramNames.indexOf(argNames.get(i))) : (RelDataType) paramTypes.get(i);
                if (bestMatch == null) {
                    bestMatch = paramType;
                } else {
                    int c = precList.compareTypePrecedence(bestMatch, paramType);
                    if (c < 0) {
                        bestMatch = paramType;
                    }
                }
            }
        }

        return bestMatch;
    }

    public static SqlNode getSelectListItem(SqlNode query, int i) {
        switch (query.getKind()) {
            case SELECT:
                SqlSelect select = (SqlSelect) query;
                SqlNode from = select.getFrom();
                if (from != null) {
                    from = stripAs(from);
                    if (from.getKind() == SqlKind.VALUES) {
                        return getSelectListItem(from, i);
                    }
                }

                SqlNodeList fields = select.getSelectList();
                if (i >= fields.size()) {
                    i = 0;
                }

                return fields.get(i);
            case VALUES:
                SqlCall call = (SqlCall) query;

                assert call.operandCount() > 0 : "VALUES must have at least one operand";

                SqlCall row = (SqlCall) call.operand(0);

                assert row.operandCount() > i : "VALUES has too few columns";

                return row.operand(i);
            default:
                throw Util.needToImplement(query);
        }
    }

    public static String deriveAliasFromOrdinal(int ordinal) {
        return "EXPR$" + ordinal;
    }

    public static boolean isGeneratedAlias(String alias) {
        return ((String) Objects.requireNonNull(alias, "alias")).toUpperCase(Locale.ROOT).startsWith("EXPR$");
    }

    public static String getOperatorSignature(SqlOperator op, List<?> typeList) {
        return getAliasedSignature(op, op.getName(), typeList);
    }

    public static String getAliasedSignature(SqlOperator op, String opName, List<?> typeList) {
        StringBuilder ret = new StringBuilder();
        String template = op.getSignatureTemplate(typeList.size());
        if (null == template) {
            ret.append("'");
            ret.append(opName);
            ret.append("(");

            for (int i = 0; i < typeList.size(); ++i) {
                if (i > 0) {
                    ret.append(", ");
                }

                String t = String.valueOf(typeList.get(i)).toUpperCase(Locale.ROOT);
                ret.append("<").append(t).append(">");
            }

            ret.append(")'");
        } else {
            Object[] values = new Object[typeList.size() + 1];
            values[0] = opName;
            ret.append("'");

            for (int i = 0; i < typeList.size(); ++i) {
                String t = String.valueOf(typeList.get(i)).toUpperCase(Locale.ROOT);
                values[i + 1] = "<" + t + ">";
            }

            ret.append((new MessageFormat(template, Locale.ROOT)).format(values));
            ret.append("'");

            assert typeList.size() + 1 == values.length;
        }

        return ret.toString();
    }

    public static CalciteException newContextException(SqlParserPos pos, Resources.ExInst<?> e, String inputText) {
        CalciteContextException ex = newContextException(pos, e);
        ex.setOriginalStatement(inputText);
        return ex;
    }

    public static CalciteContextException newContextException(SqlParserPos pos, Resources.ExInst<?> e) {
        int line = pos.getLineNum();
        int col = pos.getColumnNum();
        int endLine = pos.getEndLineNum();
        int endCol = pos.getEndColumnNum();
        return newContextException(line, col, endLine, endCol, e);
    }

    public static CalciteContextException newContextException(int line, int col, int endLine, int endCol, Resources.ExInst<?> e) {
        CalciteContextException contextExcn = (CalciteContextException) (line == endLine && col == endCol ? Static.RESOURCE.validatorContextPoint(line, col) : Static.RESOURCE.validatorContext(line, col, endLine, endCol)).ex(e.ex());
        contextExcn.setPosition(line, col, endLine, endCol);
        return contextExcn;
    }

    public static boolean isCallTo(SqlNode node, SqlOperator operator) {
        return node instanceof SqlCall && ((SqlCall) node).getOperator() == operator;
    }

    public static RelDataType createNlsStringType(RelDataTypeFactory typeFactory, NlsString str) {
        Charset charset = str.getCharset();
        if (null == charset) {
            charset = typeFactory.getDefaultCharset();
        }

        SqlCollation collation = str.getCollation();
        if (null == collation) {
            collation = SqlCollation.COERCIBLE;
        }

        RelDataType type = typeFactory.createSqlType(SqlTypeName.CHAR, str.getValue().length());
        type = typeFactory.createTypeWithCharsetAndCollation(type, charset, collation);
        return type;
    }

    public static @Nullable String translateCharacterSetName(String name) {
        switch (name) {
            case "BIG5":
                return "Big5";
            case "LATIN1":
                return "ISO-8859-1";
            case "UTF8":
                return "UTF-8";
            case "UTF16":
            case "UTF-16":
                return ConversionUtil.NATIVE_UTF16_CHARSET_NAME;
            case "GB2312":
            case "GBK":
            case "UTF-16BE":
            case "UTF-16LE":
            case "ISO-8859-1":
            case "UTF-8":
                return name;
            default:
                return null;
        }
    }

    public static Charset getCharset(String charsetName) {
        String javaCharsetName = translateCharacterSetName(((String) Objects.requireNonNull(charsetName, "charsetName")).toUpperCase(Locale.ROOT));
        if (javaCharsetName == null) {
            throw new UnsupportedCharsetException(charsetName);
        } else {
            return Charset.forName(javaCharsetName);
        }
    }

    public static void validateCharset(ByteString value, Charset charset) {
        if (charset == StandardCharsets.UTF_8) {
            byte[] bytes = value.getBytes();
            if (!Utf8.isWellFormed(bytes)) {
                String string = new String(bytes, charset);
                throw (CalciteException) Static.RESOURCE.charsetEncoding(string, charset.name()).ex();
            }
        }

    }

    public static @PolyNull SqlNode stripAs(@PolyNull SqlNode node) {
        return node != null && node.getKind() == SqlKind.AS ? ((SqlCall) node).operand(0) : node;
    }

    public static SqlNodeList stripListAs(SqlNodeList nodeList) {
        for (int i = 0; i < nodeList.size(); ++i) {
            SqlNode n = nodeList.get(i);
            SqlNode n2 = stripAs(n);
            if (n != n2) {
                nodeList.set(i, n2);
            }
        }

        return nodeList;
    }

    public static ImmutableList<SqlNode> getAncestry(SqlNode root, Predicate<SqlNode> predicate, Predicate<SqlNode> postPredicate) {
        try {
            (new Genealogist(predicate, postPredicate)).visitChild(root);
            throw new AssertionError("not found: " + predicate + " in " + root);
        } catch (Util.FoundOne e) {
            return (ImmutableList) Objects.requireNonNull(e.getNode(), "Genealogist result");
        }
    }

    public static List<RelHint> getRelHint(HintStrategyTable hintStrategies, @Nullable SqlNodeList sqlHints) {
        if (sqlHints != null && !sqlHints.isEmpty()) {
            ImmutableList.Builder<RelHint> relHints = ImmutableList.builder();

            for (SqlNode node : sqlHints) {
                assert node instanceof SqlHint;

                SqlHint sqlHint = (SqlHint) node;
                String hintName = sqlHint.getName();
                RelHint.Builder builder = RelHint.builder(hintName);
                switch (sqlHint.getOptionFormat()) {
                    case EMPTY:
                        break;
                    case LITERAL_LIST:
                    case ID_LIST:
                        builder.hintOptions(sqlHint.getOptionList());
                        break;
                    case KV_LIST:
                        builder.hintOptions(sqlHint.getOptionKVPairs());
                        break;
                    default:
                        throw new AssertionError("Unexpected hint option format");
                }

                RelHint relHint = builder.build();
                if (hintStrategies.validateHint(relHint)) {
                    relHints.add(relHint);
                }
            }

            return relHints.build();
        } else {
            return ImmutableList.of();
        }
    }

    public static RelNode attachRelHint(HintStrategyTable hintStrategies, List<RelHint> hints, Hintable rel) {
        List<RelHint> relHints = hintStrategies.apply(hints, (RelNode) rel);
        return !relHints.isEmpty() ? rel.attachHints(relHints) : (RelNode) rel;
    }

    public static SqlNode createCall(SqlOperator op, SqlParserPos pos, List<SqlNode> operands) {
        switch (op.kind) {
            case OR:
            case AND:
                switch (operands.size()) {
                    case 0:
                        return SqlLiteral.createBoolean(op.kind == SqlKind.AND, pos);
                    case 1:
                        return (SqlNode) operands.get(0);
                    case 2:
                    case 3:
                    case 4:
                    case 5:
                        break;
                    default:
                        return createBalancedCall(op, pos, operands, 0, operands.size());
                }
            default:
                return (SqlNode) (op instanceof SqlBinaryOperator && operands.size() > 2 ? createLeftCall(op, pos, operands) : op.createCall(pos, operands));
        }
    }

    private static SqlNode createLeftCall(SqlOperator op, SqlParserPos pos, List<SqlNode> nodeList) {
        SqlNode node = op.createCall(pos, nodeList.subList(0, 2));

        for (int i = 2; i < nodeList.size(); ++i) {
            node = op.createCall(pos, new SqlNode[]{node, (SqlNode) nodeList.get(i)});
        }

        return node;
    }

    private static SqlNode createBalancedCall(SqlOperator op, SqlParserPos pos, List<SqlNode> operands, int start, int end) {
        assert start < end && end <= operands.size();

        if (start + 1 == end) {
            return (SqlNode) operands.get(start);
        } else {
            int mid = (end - start) / 2 + start;
            SqlNode leftNode = createBalancedCall(op, pos, operands, start, mid);
            SqlNode rightNode = createBalancedCall(op, pos, operands, mid, end);
            return op.createCall(pos, new SqlNode[]{leftNode, rightNode});
        }
    }

    public static boolean containsIn(SqlNode node) {
        Predicate<SqlCall> callPredicate = (call) -> call.getOperator() instanceof SqlInOperator;
        return containsCall(node, callPredicate);
    }

    public static boolean containsDefault(SqlNode node) {
        Predicate<SqlCall> callPredicate = (call) -> call.getKind() == SqlKind.DEFAULT;
        return containsCall(node, callPredicate);
    }

    public static boolean containsAgg(SqlNode node) {
        Predicate<SqlCall> callPredicate = (call) -> call.getOperator().isAggregator();
        return containsCall(node, callPredicate);
    }

    public static boolean containsCall(SqlNode node, final Predicate<SqlCall> callPredicate) {
        try {
            SqlVisitor<Void> visitor = new SqlBasicVisitor<Void>() {
                public Void visit(SqlCall call) {
                    if (callPredicate.test(call)) {
                        throw new Util.FoundOne(call);
                    } else {
                        return (Void) super.visit(call);
                    }
                }
            };
            node.accept(visitor);
            return false;
        } catch (Util.FoundOne e) {
            Util.swallow(e, (Logger) null);
            return true;
        }
    }

    public static class DatabaseMetaDataInvocationHandler extends BarfingInvocationHandler {
        private final String databaseProductName;
        private final String identifierQuoteString;

        public DatabaseMetaDataInvocationHandler(String databaseProductName, String identifierQuoteString) {
            this.databaseProductName = databaseProductName;
            this.identifierQuoteString = identifierQuoteString;
        }

        public String getDatabaseProductName() {
            return this.databaseProductName;
        }

        public String getIdentifierQuoteString() {
            return this.identifierQuoteString;
        }
    }

    private static class Genealogist extends SqlBasicVisitor<Void> {
        private final List<SqlNode> ancestors = new ArrayList();
        private final Predicate<SqlNode> predicate;
        private final Predicate<SqlNode> postPredicate;

        Genealogist(Predicate<SqlNode> predicate, Predicate<SqlNode> postPredicate) {
            this.predicate = predicate;
            this.postPredicate = postPredicate;
        }

        private Void check(SqlNode node) {
            this.preCheck(node);
            this.postCheck(node);
            return null;
        }

        private Void preCheck(SqlNode node) {
            if (this.predicate.test(node)) {
                throw new Util.FoundOne(ImmutableList.copyOf(this.ancestors));
            } else {
                return null;
            }
        }

        private Void postCheck(SqlNode node) {
            if (this.postPredicate.test(node)) {
                throw new Util.FoundOne(ImmutableList.copyOf(this.ancestors));
            } else {
                return null;
            }
        }

        private void visitChild(@Nullable SqlNode node) {
            if (node != null) {
                this.ancestors.add(node);
                node.accept(this);
                this.ancestors.remove(this.ancestors.size() - 1);
            }
        }

        public Void visit(SqlIdentifier id) {
            return this.check(id);
        }

        public Void visit(SqlCall call) {
            this.preCheck(call);

            for (SqlNode node : call.getOperandList()) {
                this.visitChild(node);
            }

            return this.postCheck(call);
        }

        public Void visit(SqlIntervalQualifier intervalQualifier) {
            return this.check(intervalQualifier);
        }

        public Void visit(SqlLiteral literal) {
            return this.check(literal);
        }

        public Void visit(SqlNodeList nodeList) {
            this.preCheck(nodeList);

            for (SqlNode node : nodeList) {
                this.visitChild(node);
            }

            return this.postCheck(nodeList);
        }

        public Void visit(SqlDynamicParam param) {
            return this.check(param);
        }

        public Void visit(SqlDataTypeSpec type) {
            return this.check(type);
        }
    }
}
